/*
 * Created on Feb 13, 2005
 * Created by dfhuynh
 */
package edu.mit.simile.longwell.query.engine;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.log4j.Logger;

import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.vocabulary.RDF;

import edu.mit.simile.longwell.CascadedCache;
import edu.mit.simile.longwell.LongwellServer;
import edu.mit.simile.longwell.LongwellServlet;
import edu.mit.simile.longwell.Profile;
import edu.mit.simile.longwell.query.Facet;
import edu.mit.simile.longwell.query.INarrower;
import edu.mit.simile.longwell.query.QueryException;
import edu.mit.simile.longwell.query.bucket.IBucketer;
import edu.mit.simile.longwell.query.engine.EngineUtilities.FacetInfo;
import edu.mit.simile.longwell.query.project.IProjection;
import edu.mit.simile.longwell.query.project.IProjector;
import edu.mit.simile.longwell.query.project.URIProjector;
import edu.mit.simile.longwell.schema.ISchemaModel;
import edu.mit.simile.longwell.schema.IStructuredProperty;

/**
 * @author dfhuynh
 */
class Narrower implements INarrower {
	final static private Logger s_logger =
		Logger.getLogger(Narrower.class);
	
	final protected Profile 		m_profile;
	final protected QueryEngine		m_engine;
	final protected CascadedCache	m_explicitFacetStringToObjectsToRecords =
		new CascadedCache(
			Narrower.class,
			"explicitFacetString", 10,
			"objects", 10
		);
	final protected CascadedCache	m_explicitFacetStringToObjectsToGuessedRecords =
		new CascadedCache(
			Narrower.class,
			"explicitFacetString", 10,
			"objects", 10
		);
	
	protected class NarrowingRecord {
		String	m_propertyURI;
		boolean	m_explicit;
		float	m_desirable;
		
		Facet	m_facet;
	}
	
	Narrower(Profile profile, QueryEngine engine) {
		m_profile = profile;
		m_engine = engine;
	}

	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.query.INarrower#narrow(java.util.Set, java.util.SortedSet, java.lang.String, boolean)
	 */
	public List narrow(Set objects, SortedSet explicitFacets, String locale, boolean fresh) throws QueryException {
		String explicitFacetString = "";
		Iterator i = explicitFacets.iterator();
		while (i.hasNext()) {
			explicitFacetString += ((String) i.next()) + ";";
		}
		
		List records = (List) 
			m_explicitFacetStringToObjectsToRecords.get(
				explicitFacetString, objects);
		
		if (records == null || fresh) {
			List guessedRecords = (List) 
				m_explicitFacetStringToObjectsToGuessedRecords.get(
					explicitFacetString, objects);
			if (guessedRecords == null || fresh) {
				guessedRecords = internalNarrowByGuessing(
					objects, explicitFacets, locale);
				
				m_explicitFacetStringToObjectsToGuessedRecords.put(
					explicitFacetString, objects, guessedRecords);
			} else {
				updateRecords(guessedRecords, objects, locale);
			}
			
			records = internalNarrowExplicitFacets(objects, explicitFacets, locale);
			records.addAll(guessedRecords);
			
			m_explicitFacetStringToObjectsToRecords.put(
				explicitFacetString, objects, records);
		} else {
			updateRecords(records, objects, locale);
		}
		
		List facets = new ArrayList();
		
		i = records.iterator();
		while (i.hasNext()) {
			facets.add(((NarrowingRecord) i.next()).m_facet);
		}

		return facets;
	}
	
	protected List internalNarrowExplicitFacets(Set objects, Set explicitFacets, String locale) {
		Set records = createNarrowingRecordTreeSet();
		ISchemaModel schemaModel = m_profile.getSchemaModel();
		Model model = m_profile.getModel();
		
		Iterator i = explicitFacets.iterator();
		while (i.hasNext()) {
			String propertyURI = (String) i.next();
			IStructuredProperty sProperty = schemaModel.getStructuredProperty(
				model.createProperty(propertyURI));
			
			try {
				createFacet(objects, sProperty, locale, null, records, 1, true);
			} catch (QueryException e) {
				s_logger.warn("Failed to create facet", e);
			}
		}
		
		return new ArrayList(records);
	}

	protected List internalNarrowByGuessing(Set objects, Set explicitFacets, String locale) {
		List precedentFacets = new ArrayList();
		Set otherFacets = createNarrowingRecordTreeSet();
		
		final int maxCount = LongwellServer.getDefaultFacetsNumber();
		
		Iterator i = m_profile.getSchemaModel()
			.getSortedStructuredProperties(objects).iterator();
		while (i.hasNext() && precedentFacets.size() + otherFacets.size() < maxCount) {
			IStructuredProperty sProperty = (IStructuredProperty) i.next();
			if (!explicitFacets.contains(sProperty.getURI())) {
				int count = precedentFacets.size() + otherFacets.size();
				
				try {
					boolean createfacet = createFacet(
							objects, sProperty, locale, 
							precedentFacets, otherFacets,
							(maxCount - count - 1) / (float) maxCount,
							false);
							
					if (!createfacet ) {
						break;
					}
					count++;
				} catch (QueryException e) {
					s_logger.warn("Failed to create facet", e);
				}
			}
		}
		
		precedentFacets.addAll(otherFacets);
		
		return precedentFacets;
	}
	
	protected void updateRecords(List guessedRecords, Set objects, String locale) 
			throws QueryException {
		Iterator i = guessedRecords.iterator();
		while (i.hasNext()) {
			NarrowingRecord record = (NarrowingRecord) i.next();
			
			updateRecord(record, objects, locale);
		}
	}

	protected boolean createFacet(
			Set 				objects, 
			IStructuredProperty sProperty, 
			String 				locale, 
			List 				precedentRecords, 
			Set 				otherRecords,
			float 				desirable,
			boolean				explicit
		) throws QueryException {
		FacetInfo info = EngineUtilities.getFacetInfo(sProperty, true, locale, m_profile);
		
		IProjector projector = m_engine.getProjectorManager().getProjector(
			info.m_projectorName, info.m_projectorParameter, locale);
		
		IProjection projection = projector.project(objects);
		s_logger.info(
			"Considering facet property " + sProperty.getURI() + 
			" " + sProperty.getUniqueness() + 
			" " + sProperty.countOccurrences() + 
			" " + (objects.size() / 10) + 
			//" " + projection.countObjects((Object) null) + 
			" " + (objects.size() / 3));
		
		// TODO magic
		int count = objects.size();
		if (desirable < 1 && 
			sProperty.countOccurrences() < count / 5 + 20 * Math.exp(-count / 20)) {
			
			return false;
		}
		
		/*
		 * Get bucketing suggestions
		 */
		IBucketer bucketer = m_engine.getBucketerManager().getBucketer(info.m_bucketerName);
		//List bucketThemes = bucketer.suggestNarrowingBuckets(projection, 1);
		List bucketThemes = bucketer.suggestNarrowingBuckets(projection, desirable);
		if (explicit || bucketThemes.size() > 0) {
			String propertyURI = sProperty.getURI();
			Facet facet = explicit ? 
				new ExplicitFacet(
				info.m_projectorName,
				info.m_projectorParameter,
					info.m_label,
					bucketThemes,
					propertyURI
				) :
				new Facet(
					info.m_projectorName,
					info.m_projectorParameter,
					info.m_label,
					bucketThemes,
					false
				);

			NarrowingRecord record = new NarrowingRecord();
			record.m_propertyURI = propertyURI;
			record.m_explicit = explicit;
			record.m_desirable = desirable;
			record.m_facet = facet;
			
			if (precedentRecords != null && 
				sProperty.getURI().equals(RDF.type.getURI())) {
				
				precedentRecords.add(record);
			} else {
				otherRecords.add(record);
			}
		}
		
		return true;
	}
	
	protected void updateRecord(NarrowingRecord record, Set objects, String locale) 
			throws QueryException {
		FacetInfo info = EngineUtilities.getFacetInfo(record.m_propertyURI, true, locale, m_profile);
		
		IProjector projector = m_engine.getProjectorManager().getProjector(
			info.m_projectorName, info.m_projectorParameter, locale);
			
		IProjection projection = projector.project(objects);
		
		IBucketer bucketer = m_engine.getBucketerManager().getBucketer(info.m_bucketerName);
		List bucketThemes = bucketer.suggestNarrowingBuckets(projection, record.m_desirable);

		record.m_facet = record.m_explicit ? 
			new ExplicitFacet(
				info.m_projectorName,
				info.m_projectorParameter,
				info.m_label,
				bucketThemes,
				record.m_propertyURI
			) :
			new Facet(
				info.m_projectorName,
				info.m_projectorParameter,
				info.m_label,
				bucketThemes,
				false
			);
	}
	
	protected TreeSet createNarrowingRecordTreeSet() {
		return new TreeSet(new Comparator() {
			public boolean equals(Object obj) {
				return false;
			}

			public int compare(Object o1, Object o2) {
				NarrowingRecord	r1 = (NarrowingRecord) o1;
				NarrowingRecord	r2 = (NarrowingRecord) o2;
				Facet f1 = r1.m_facet;
				Facet f2 = r2.m_facet;
				int i = f1.m_label.compareToIgnoreCase(f2.m_label);
				
				if (i == 0) {
					i = f1.m_projectorName.compareTo(f2.m_projectorName);
				}
				if (i == 0) {
					i = o1.toString().compareTo(o2.toString());
				}
				return i;
			}
		});
	}
	
	static public class ExplicitFacet extends Facet {
		String m_propertyURI;
		
		public ExplicitFacet(
				String projectorName, 
				String projectorParameter, 
				String label, 
				List bucketThemes, 
				String propertyURI
			) {
			super(projectorName, projectorParameter, label, bucketThemes, true);
			m_propertyURI = propertyURI;
		}
		
		public String getPropertyURI() {
			return m_propertyURI;
		}
	}
}
